/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sentry;

import io.sentry.protocol.SentryId;
import io.sentry.util.Objects;

import java.util.Date;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * Span
 *
 * @since 2021-06-02
 **/
@ApiStatus.Internal
public final class Span implements ISpan {
    /**
     * The moment in time when span was started.
     */
    private final @NotNull Date startTimestamp;
    /**
     * The moment in time when span has ended.
     */
    private @Nullable Date timestamp;

    private final @NotNull SpanContext context;

    /**
     * A transaction this span is attached to. Marked as transient to be ignored during JSON
     * serialization.
     */
    private final @NotNull SentryTracer transaction;

    /**
     * A throwable thrown during the execution of the span.
     */
    private @Nullable Throwable throwable;

    private final @NotNull IHub hub;

    private final @NotNull AtomicBoolean finished = new AtomicBoolean(false);

    Span(
        final @NotNull SentryId traceId,
        final @Nullable SpanId parentSpanId,
        final @NotNull SentryTracer transaction,
        final @NotNull String operation,
        final @NotNull IHub hub) {
        this.context =
            new SpanContext(traceId, new SpanId(), operation, parentSpanId, transaction.isSampled());
        this.transaction = Objects.requireNonNull(transaction, "transaction is required");
        this.startTimestamp = DateUtils.getCurrentDateTime();
        this.hub = Objects.requireNonNull(hub, "hub is required");
    }

    Span(
        final @NotNull TransactionContext context,
        final @NotNull SentryTracer sentryTracer,
        final @NotNull IHub hub) {
        this.context = Objects.requireNonNull(context, "context is required");
        this.transaction = Objects.requireNonNull(sentryTracer, "sentryTracer is required");
        this.hub = Objects.requireNonNull(hub, "hub is required");
        this.startTimestamp = DateUtils.getCurrentDateTime();
    }

    public @NotNull Date getStartTimestamp() {
        return (Date) startTimestamp.clone();
    }

    public @Nullable Date getTimestamp() {
        return (Date) timestamp.clone();
    }

    @Override
    public @NotNull ISpan startChild(final @NotNull String operation) {
        return this.startChild(operation, null);
    }

    @Override
    public @NotNull ISpan startChild(
        final @NotNull String operation, final @Nullable String description) {
        return transaction.startChild(context.getSpanId(), operation, description);
    }

    @Override
    public @NotNull SentryTraceHeader toSentryTrace() {
        return new SentryTraceHeader(context.getTraceId(), context.getSpanId(), context.getSampled());
    }

    @Override
    public void finish() {
        this.finish(this.context.getStatus());
    }

    @Override
    public void finish(@Nullable SpanStatus status) {
        // the span can be finished only once
        if (!finished.compareAndSet(false, true)) {
            return;
        }

        this.context.setStatus(status);
        timestamp = DateUtils.getCurrentDateTime();
        if (throwable != null) {
            hub.setSpanContext(throwable, this, this.transaction.getName());
        }
    }

    @Override
    public void setOperation(final @NotNull String operation) {
        this.context.setOperation(operation);
    }

    @Override
    public @NotNull String getOperation() {
        return this.context.getOperation();
    }

    @Override
    public void setDescription(final @Nullable String description) {
        this.context.setDescription(description);
    }

    @Override
    public @Nullable String getDescription() {
        return this.context.getDescription();
    }

    @Override
    public void setStatus(final @Nullable SpanStatus status) {
        this.context.setStatus(status);
    }

    @Override
    public @Nullable SpanStatus getStatus() {
        return this.context.getStatus();
    }

    @Override
    public @NotNull SpanContext getSpanContext() {
        return context;
    }

    @Override
    public void setTag(final @NotNull String key, final @NotNull String value) {
        this.context.setTag(key, value);
    }

    @Override
    public @Nullable String getTag(@NotNull String key) {
        return context.getTags().get(key);
    }

    @Override
    public boolean isFinished() {
        return finished.get();
    }

    /**
     * isSampled
     *
     * @return Boolean
     */
    public @Nullable Boolean isSampled() {
        return context.getSampled();
    }

    @Override
    public void setThrowable(final @Nullable Throwable throwable) {
        this.throwable = throwable;
    }

    @Override
    public @Nullable Throwable getThrowable() {
        return throwable;
    }

    /**
     * getTraceId
     *
     * @return SentryId
     */
    @NotNull
    public SentryId getTraceId() {
        return context.getTraceId();
    }

    /**
     * getSpanId
     *
     * @return SpanId
     */
    public @NotNull SpanId getSpanId() {
        return context.getSpanId();
    }

    /**
     * getParentSpanId
     *
     * @return SpanId
     */
    public @Nullable SpanId getParentSpanId() {
        return context.getParentSpanId();
    }

    /**
     * getTags
     *
     * @return Map
     */
    public Map<String, String> getTags() {
        return context.getTags();
    }
}
